import math, random, weakref, re, os
import pyglet
from pyglet.gl import *
import pymunk
from pymunk import Space, Body, inf, Segment, Poly, Vec2d
from gator import Gator
from baby import Baby
from gameprops import *
from util import *

if pymunk.version < '1.1':
    def point_query(self, point, layers = -1, group = 0):
        self.__query_hits = []
        def cf(_shape, data):
            
            shape = self._get_shape(_shape)
            self.__query_hits.append(shape)
        f = pymunk._chipmunk.cpSpacePointQueryFunc(cf)
        pymunk._chipmunk.cpSpacePointQuery(self._space, point, layers, group, f, None)
        
        return self.__query_hits
    
    def _get_shape(self, _shape):
        if not bool(_shape):
            return None
        hashid = _shape.contents.hashid
        if hashid in self._shapes:
            shape = self._shapes[hashid]
        elif hashid in self._static_shapes:
            shape = self._static_shapes[hashid]
        return shape

    pymunk.Space.point_query = point_query
    pymunk.Space._get_shape = _get_shape


class MainWindow(pyglet.window.Window):
    def __init__(self):
        super(MainWindow, self).__init__(resizable=True, visible=False, fullscreen=False)
        self.set_caption('See You Later')
        
        self.keys = pyglet.window.key.KeyStateHandler()

        self.push_handlers(self.keys)

        self.menu_controller = GameMenu(self)
        self.start_game(1)

        pyglet.clock.schedule_interval(self.update, 1/60.0)
        
        glDisable(GL_DEPTH_TEST)
        glEnable(GL_CULL_FACE)
        glDisable(GL_DITHER)
        glEnable(GL_BLEND)
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
        glClearColor(0,0,0,1)

        self.show_menu()
        

    def start_game(self, num_babies):
        self.num_babies = num_babies

        if hasattr(self, 'game_controller') and self.game_controller:
            del self.active_controller
            self.game_controller.destroy()
            del self.game_controller

        self.game_controller = GameController(self, self.num_babies)
        self.game_controller.on_resize(self.width, self.height)
        self.push_handlers(self.game_controller)
        self.active_controller = self.game_controller

    def on_draw(self):
        self.active_controller.draw()
        
    def update(self, dt):
        self.active_controller.update(dt)

    def time_travel(self, time_machine):
        gator = self.game_controller.gator
        if gator.held_baby:
            time_machine.add_baby(gator.held_baby)
            gator.held_baby = None
        timeTravel = TimeTravel(self, gator, time_machine.babies)
        timeTravel.on_resize(self.width, self.height)
        self.pop_handlers()
        self.push_handlers(timeTravel)
        self.active_controller = timeTravel

    def show_menu(self):
        self.menu_controller.on_resize(self.width, self.height)
        self.push_handlers(self.menu_controller)
        self.active_controller = self.menu_controller

    def back_to_game(self):
        self.pop_handlers()
        self.active_controller = self.game_controller

class GameMap(object):
    def __init__(self, window):
        self.window = window

        self.space = Space(10,10)
        self.space.gravity = (0.0, -4000.0)
        self.space.damping = 0.01
        self.space.resize_static_hash()
        self.space.resize_active_hash()

        self.static_body = Body(inf, inf)
        self.segments = []
        self.polys = []
        self.objects = []

        # Collision types:
        # 0 = ground, walls, etc.
        # 1 = gator
        # 2 = babies
        # 3 = levers & other props (which are sensors)
        # 4 = bridge
        self.space.add_collision_handler(1, 0, self.gator_collision, None, None, None)
        self.space.add_collision_handler(1, 2, self.gator_collision, None, None, None)
        self.space.add_collision_handler(1, 3, self.gameprop_collision, None, None, None)
        self.space.add_collision_handler(2, 3, self.gameprop_collision, None, None, None)
        self.space.add_collision_handler(2, 0, None, None, self.baby_collision, None)
        self.space.add_collision_handler(1, 4, self.bridge_collision, None, None, self.bridge_uncollision)
        self.space.add_collision_handler(2, 4, self.bridge_collision, None, None, self.bridge_uncollision)

        self.add_rect(2048,0,1500,100) # initial area ground
        self.add_rect(0,0,100,2048) # left wall
        self.add_rect(2556,516,474,78) # first ledge

        ladder1 = Ladder(self, 2498,110)
        self.add_object(ladder1)
        self.add_object(Lever(self,3098,110, lambda: ladder1.extend()))

        self.add_rect(3237,0,1139,580) # mesa ground
        self.add_rect(5079,0,259,580) # mesa ground
        self.add_rect(4117,0,1178,55) # floor below bridge

        self.add_object(WeakBridge(self,4378,464))

        self.add_object(TimeMachine(self, 4739, 38, 8)) # below the bridge
        
        self.add_rect(3943,1007,474,78) # second ledge
        self.add_rect(4588,1007,474,78) # third ledge

        ladder2 = Ladder(self,3883,588)
        self.add_object(ladder2)
        self.add_object(Lever(self,4188,1087, lambda: ladder2.extend()))

        self.add_rect(5228,0,922,1085) # left side of mountain
        self.add_rect(5455,1021,151,132) # rock near big head

        bighead = BigHead(self, 6183, 1038)
        self.add_object(bighead)
        self.add_object(Lever(self,6458,1090, lambda: bighead.open_mouth()))

        self.add_rect(5228,664,2141,421)  # floor of mountain tunnel
        self.add_rect(6116,1396,1273,469) # top of mountain
        self.add_rect(6439,0,922,134) # floor of time cave
        self.add_rect(6078,0,463,100) # floor of time cave
        self.add_rect(7032,483,337,429) # top of cave entrance
        self.add_rect(7006,0,521,202) # bottom of cave entrance
        self.add_rect(6438,0,197,213) # rock inside cave

        self.add_object(TimeMachine(self, 6325, 100, 1)) # inside cave

        self.add_rect(7503,899,481,80) # outside ledge
        self.add_rect(7973,491,481,80) # second ledge

        self.add_object(Ladder(self,8041,569, state='long')) # ladder between ledges
        self.add_object(Ladder(self,8515,163, state='long')) # ladder between ledges

        self.add_poly([(7036,182),(7468,230),(7816,230),(8503,80)]) # ground
        self.add_poly([(7828,0),(7858,110),(8648,164),(9348,164),(9348,0)]) # ground

        self.add_rect(9059,0,388,363) # left platform
        self.add_rect(9538,0,711,360) # right platform
        self.add_rect(9347,0,290,317) # mid platform
        self.add_rect(9204,433,125,566) # brick wall

        ladder3 = Ladder(self,9142,470) # ladder left of wall
        self.add_object(ladder3)
        self.add_object(Lever(self,9610,377, lambda: ladder3.extend()))

        self.add_object(Ladder(self,9388,470, state='long')) # ladder right of wall

        self.add_object(BabyGlue(self, 9494, 320)) # keep baby still in depression

        self.add_rect(9460,1037,743,86) # upper ledge
        self.add_rect(9672,1130,92,102) # bricks
        self.add_rect(9628,1128,178,62) # bricks
        self.add_rect(10119,0,129,2046) # right map boundary

        self.add_object(TimeMachine(self, 9968, 1139, 2)) # time machine past brick wall


        door = MushroomDoor(self, 1440,290)
        self.add_object(door)
        self.add_object(Lever(self,835,364, lambda: door.open_door()))

        self.add_rect(1585,0,474,222)  # lower step
        self.add_rect(0,0,1762,295)    # top step and ground
        self.add_rect(737,140,182,209) # platform for lever

        self.add_object(Ladder(self,453,289,state='long')) # ladder near time machine

        self.add_object(TimeMachine(self, 220, 286, 5)) # left of mushroom door

        self.load_background_tiles()

    def add_segment(self, v1, v2):
        segment = Segment(self.static_body, v1, v2, 10)
        segment.friction = 0
        self.space.add_static(segment)
        self.segments.append(segment)

    def add_rect(self, x1, y1, width, height):
        x2 = x1 + width
        y2 = y1 + height
        vertices = ((x1,y1), (x1,y2), (x2,y2), (x2,y1))
        poly = Poly(self.static_body, vertices)
        self.space.add_static(poly)
        self.polys.append(poly)

    def add_poly(self, vertices):
        poly = Poly(self.static_body, vertices)
        self.space.add_static(poly)
        self.polys.append(poly)        
        
    def load_background_tiles(self):
        self.background_tiles = {}
        path = 'bg'
        reName = re.compile('bg_(\d*)_(\d*)\.png')
        names = os.listdir(path)
        for name in names:
            m = reName.match(name)
            if m:
                x, y = map(int, m.groups())
                tile = make_sprite(path + '/' + name)
                tile.x = x*512
                tile.y = y*512
                self.background_tiles[(x,y)] = tile

    def gator_collision(self, space, arbiter, *args, **kwargs):
        # This callback is used to figure out when the gator lands back
        # down on a surface so that we can set the gator's is_midair flag
        # to False, allowing the player to jump again.
        gator = None
        for shape in arbiter.shapes:
            if shape and hasattr(shape, 'object'):
                game_object = shape.object()
                if isinstance(game_object, Gator):
                    gator = game_object
                    break
        if not gator:
            return True

        for contact in arbiter.contacts:
            # Orientation of the contact normal with other polys seems to
            # basically be random for some reason.  So we try to fix the
            # direction of the normal here.
            direction = gator.body.position - contact.position
            if direction.dot(contact.normal) > 0:
                normal = contact.normal
            else:
                normal = -contact.normal
            angle = normal.get_angle_degrees_between(Vec2d(0,1))
            #print 'gator collision', angle
            if angle < 50:
                gator.is_midair = False
        return True

    def gameprop_collision(self, space, arbiter, *args, **kwargs):
        actor = None
        other = None
        for shape in arbiter.shapes:
            if shape and hasattr(shape, 'object'):
                game_object = shape.object()
                if isinstance(game_object, Gator) or isinstance(game_object, Baby):
                    actor = game_object
                else:
                    other = game_object
        if not (actor and other):
            return True

        if isinstance(other, Lever):
            other.switch_on()
        elif isinstance(other, TimeMachine):
            if isinstance(actor, Gator):
                #print "you're going back through time!"
                self.window.time_travel(other)
            elif isinstance(actor, Baby):
                other.add_baby(actor)
                self.remove_object(actor)
        elif isinstance(other, BabyGlue):
            if isinstance(actor, Baby):
                actor.change_state('idle')
        return True

    def baby_collision(self, space, arbiter, *args, **kwargs):
        if not arbiter.is_first_contact: return

        baby = None
        for shape in arbiter.shapes:
            if shape and hasattr(shape, 'object'):
                game_object = shape.object()
                if isinstance(game_object, Baby):
                    baby = game_object
                    break
        if not baby:
            return

        if baby.state == 'crawl_right':
            direction = Vec2d(1,0)
        elif baby.state == 'crawl_left':
            direction = Vec2d(-1,0)
        else:
            return
        for contact in arbiter.contacts:
            # Want normal pointing towards the baby
            r = baby.body.position - contact.position
            if r.dot(contact.normal) > 0:
                normal = contact.normal
            else:
                normal = -contact.normal
            if normal.dot(direction) < 0:
                baby.turn_around()
                return
        
    def bridge_collision(self, space, arbiter, *args, **kwargs):
        actor = None
        bridge = None
        for shape in arbiter.shapes:
            if shape and hasattr(shape, 'object'):
                game_object = shape.object()
                if isinstance(game_object, Gator) or isinstance(game_object, Baby):
                    actor = game_object
                elif isinstance(game_object, WeakBridge):
                    bridge = game_object
        if not actor:
            return True

        if isinstance(actor, Gator):
            self.gator_collision(space, arbiter, *args, **kwargs)
        if bridge:
            bridge.add_object(actor)
        return True

    def bridge_uncollision(self, space, arbiter, *args, **kwargs):
        actor = None
        bridge = None
        for shape in arbiter.shapes:
            if shape and hasattr(shape, 'object'):
                game_object = shape.object()
                if isinstance(game_object, Gator) or isinstance(game_object, Baby):
                    actor = game_object
                elif isinstance(game_object, WeakBridge):
                    bridge = game_object
        if not actor:
            return True

        if bridge:
            bridge.remove_object(actor)
        return True

    def add_object(self, obj):
        self.space.add(*obj.get_body_and_shapes())
        obj.map = weakref.proxy(self)
        self.objects.append(obj)
        
    def remove_object(self, obj):
        self.space.remove(*obj.get_body_and_shapes())
        obj.map = None
        self.objects.remove(obj)

    def set_view_position(self, controller, position):
        x1 = max(0, position.x - 0.5*controller.view_width)
        if x1 + controller.view_width > 10240:
            x1 = 10240 - controller.view_width
        y1 = max(0, position.y - 0.35*controller.view_height)
        if y1 + controller.view_height > 2048:
            y1 = 2048 - controller.view_height
        glTranslatef(-x1, -y1, 0)
        self.rect = (x1, y1, controller.view_width, controller.view_height)

    def draw_background(self):
        x1 = int(self.rect[0] / 512)
        y1 = int(self.rect[1] / 512)
        x2 = x1 + int(self.rect[2] / 512) + 1
        y2 = y1 + int(self.rect[3] / 512) + 1
        for x in range(x1,x2+1):
            for y in range(y1,y2+1):
                if (x,y) in self.background_tiles:
                    self.background_tiles[(x,y)].draw()

    def draw(self,debug=False):
        self.draw_background()

        for obj in self.objects:
            obj.draw()

        if debug:
            glBegin(GL_LINES)
            for segment in self.segments:
                glVertex2f(segment.a.x, segment.a.y)
                glVertex2f(segment.b.x, segment.b.y)
            glEnd()

            for shape in self.polys:
                drawPolyShape(shape)

    def draw_front(self):
        for obj in self.objects:
            if hasattr(obj, 'draw_front'):
                obj.draw_front()

    def update(self, dt):
        for obj in self.objects:
            obj.update(dt)
        self.space.step(dt)

    def destroy(self):
        for obj in self.objects[:]:
            self.remove_object(obj)
            obj.destroy()
        del self.space
        del self.static_body
        del self.segments
        del self.objects
        del self.window

class GameController(object):
    def __init__(self, window, num_babies):
        self.window = window
        self.map = GameMap(window)
        self.gator = Gator(self.map)
        self.gator.body.position = (2360,300)
        if num_babies > 9: num_babies = 9
        self.babies = [Baby(i+1) for i in range(num_babies)]
        self.babies[-1].body.position = (3785,690)
        self.babies[-1].change_state('idle')
        for i in range(0, num_babies-1):
            self.babies[i].body.position = (2350,600)
            
        for baby in self.babies:
            self.map.add_object(baby)

        if num_babies >= 2:
            # Block off the first time machine.
            #self.map.add_object(Boulders(self.map, 7040,141))
            pass
        if num_babies >= 3:
            # Block off the second time machine.
            #self.map.add_object(Boulders(self.map, 6916,984))
            pass

        self.fps_display = pyglet.clock.ClockDisplay(color=(1,1,1,1))
        self.debug_draw = False
        self.debug_fps = False

        glClear(GL_COLOR_BUFFER_BIT)

    def destroy(self):
        self.gator.destroy()
        del self.gator
        self.map.destroy()
        del self.map
        del self.babies

    def on_resize(self, width, height):
        aspect_ratio = float(width) / float(height)
        self.view_width = 1200
        self.view_height = self.view_width / aspect_ratio
        glViewport(0, 0, width, height)
        self.set_projection()

    def set_projection(self):
        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        glOrtho(0, self.view_width, 0, self.view_height, -1, 1)
        glMatrixMode(GL_MODELVIEW)

    def update(self, dt):
        for i in range(5):
            self.gator.update(dt/5.0, self.window.keys)
            self.map.update(dt/5.0)

    def draw(self):
        self.set_projection()
        glLoadIdentity()
        self.map.set_view_position(self, self.gator.body.position)
        self.map.draw(self.debug_draw)
        self.gator.draw()
        self.map.draw_front()
        if self.debug_fps:
            glLoadIdentity()
            self.fps_display.draw()

    def on_key_press(self, symbol, modifier):
        if symbol == pyglet.window.key.ESCAPE:
            pyglet.app.exit()
        elif symbol == pyglet.window.key._0 or symbol == pyglet.window.key.F:
            self.window.set_fullscreen(not self.window.fullscreen)
        #elif symbol == pyglet.window.key._1:
        #    self.debug_draw = not self.debug_draw
        elif symbol == pyglet.window.key._1:
            self.debug_fps = not self.debug_fps
        elif symbol in (pyglet.window.key.LSHIFT, pyglet.window.key.RSHIFT,
                        pyglet.window.key.LCTRL,  pyglet.window.key.RCTRL):
            self.gator.action(self.window.keys)
        elif symbol == pyglet.window.key.SPACE:
            self.gator.jump()
        elif symbol == pyglet.window.key.S:
            pyglet.image.get_buffer_manager().get_color_buffer().save('screenshot.png')
        elif symbol in (pyglet.window.key.M, pyglet.window.key.H):
            self.window.show_menu()

class TimeTravel(object):
    def __init__(self, window, gator, babies):
        self.window = window
        self.gator = gator
        self.gator.body.position = (0,0)
        self.babies = babies
        for baby in self.babies:
            radius = random.uniform(50,400)
            angle = random.uniform(0,359)
            baby.body.position = Vec2d(0,radius).rotated_degrees(angle)

        self.fps_display = pyglet.clock.ClockDisplay(color=(1,1,1,1))

        self.time_spiral = make_sprite('graphics/time_spiral.png', 391, 400)
        self.time_spiral.position = (0,0)
        self.angle = 0
        self.scale = 10
        self.t = 0

    def on_resize(self, width, height):
        aspect_ratio = float(width) / float(height)
        self.view_width = 1200
        self.view_height = self.view_width / aspect_ratio
        glViewport(0, 0, width, height)
        self.set_projection()
        return pyglet.event.EVENT_HANDLED

    def set_projection(self):
        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        glOrtho(-0.5*self.view_width, 0.5*self.view_width, -0.5*self.view_height, 0.5*self.view_height, -1, 1)
        glMatrixMode(GL_MODELVIEW)

    def update(self, dt):
        self.t += dt
        self.angle += -50*dt
        self.scale = 10*math.exp(-self.t)
        self.gator.sprite.scale = self.scale
        for baby in self.babies:
            baby.sprite.scale = self.scale
        if self.scale < 0.1:
            self.exit_time_machine()

    def exit_time_machine(self):
        self.window.pop_handlers()
        self.window.start_game(len(self.babies)+1)

    def draw(self):
        self.set_projection()
        glLoadIdentity()
        glClear(GL_COLOR_BUFFER_BIT)
        glRotatef(self.angle, 0, 0, 1)
        self.time_spiral.draw()
        self.gator.draw()
        for baby in self.babies:
            baby.draw()
        #glLoadIdentity()
        #self.fps_display.draw()

    def on_key_press(self, symbol, modifier):
        if symbol == pyglet.window.key.ESCAPE:
            pyglet.app.exit()
        elif symbol == pyglet.window.key._0:
            self.window.set_fullscreen(not self.window.fullscreen)
        return pyglet.event.EVENT_HANDLED

class GameMenu(object):
    def __init__(self, window):
        self.window = window
        self.labels = [pyglet.text.Label('See You Later!',font_size=64,anchor_x='center',x=0,y=250,bold=True,color=(50,225,50,255)),
                       pyglet.text.Label('A game by Phillip Nguyen for PyWeek#12',font_size=32,anchor_x='center',x=0,y=200,color=(50,225,50,255)),
                       pyglet.text.Label('Press LEFT/RIGHT to move,\n SPACE to jump.'),
                       pyglet.text.Label('Use UP/DOWN to climb ladders.'),
                       pyglet.text.Label('Pick up and throw with SHIFT or CTRL.'),
                       pyglet.text.Label('SHIFT+UP to stack held items on your hat.'),
                       pyglet.text.Label('SHIFT+DOWN to stand on held items.'),
                       pyglet.text.Label('Press F to toggle fullscreen mode.'),
                       pyglet.text.Label('Press M for this screen.'),
                       pyglet.text.Label('Press ESC to quit.')]

        y = 100
        for label in self.labels[2:7]:
            label.anchor_x = 'left'
            label.font_size = 24
            label.x = -350
            label.y = y
            y -= 50

        y -= 40
        for label in self.labels[7:]:
            label.anchor_x = 'left'
            label.font_size = 24
            label.x = -350
            label.y = y
            y -= 50

    def on_resize(self, width, height):
        aspect_ratio = float(width) / float(height)
        self.view_width = 1200
        self.view_height = self.view_width / aspect_ratio
        glViewport(0, 0, width, height)
        self.set_projection()

    def set_projection(self):
        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        glOrtho(-0.5*self.view_width, 0.5*self.view_width, -0.5*self.view_height, 0.5*self.view_height, -1, 1)
        glMatrixMode(GL_MODELVIEW)

    def update(self, dt):
        pass

    def draw(self):
        self.window.game_controller.draw()
        self.set_projection()
        glLoadIdentity()
        glColor4f(0,0,0,0.7)
        x1 = -0.5*self.view_width
        x2 = -x1
        y1 = -0.5*self.view_height
        y2 = -y1
        pyglet.graphics.draw(4, GL_QUADS, ('v2f', (x1, y1, x2, y1, x2, y2, x1, y2)))
        for label in self.labels: label.draw()
        
    def on_key_press(self, symbol, modifier):
        if symbol == pyglet.window.key.ESCAPE:
            pyglet.app.exit()
        elif symbol in (pyglet.window.key._0, pyglet.window.key.F) :
            self.window.set_fullscreen(not self.window.fullscreen)
        else:
            self.window.back_to_game()

        return pyglet.event.EVENT_HANDLED

def run_game():
    if pymunk.version < '1.1':
        pymunk.init_pymunk()
    window = MainWindow()
    window.clear()
    window.flip()
    window.set_visible(True)
    pyglet.app.run()

if __name__ == '__main__':
    run_game()
